package org.gudy.azureus2.core3.download.impl;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.savelocation.DefaultSaveLocationManager;
import org.gudy.azureus2.plugins.download.savelocation.SaveLocationChange;
import org.gudy.azureus2.pluginsimpl.local.download.DownloadImpl;

import com.aelitis.azureus.core.tag.Tag;
import com.aelitis.azureus.core.tag.TagFeature;
import com.aelitis.azureus.core.tag.TagFeatureFileLocation;
import com.aelitis.azureus.core.tag.TagManagerFactory;

public class DownloadManagerDefaultPaths extends DownloadManagerMoveHandlerUtils {

    public final static DefaultSaveLocationManager DEFAULT_HANDLER = new DefaultSaveLocationManager() {
        public SaveLocationChange onInitialization(Download d, boolean for_move, boolean on_event) {

            /**
             * This manager object isn't the sort of object which decides on an alternate initialisation place for a download - if a user has chosen a
             * path for it, we don't interfere with it under any circumstances (though if plugins want to, then that's up to them).
             */
            if (on_event) {
                return null;
            }

            DownloadManager dm = ((DownloadImpl) d).getDownload();
            return determinePaths(dm, UPDATE_FOR_MOVE_DETAILS[1], for_move, false); // 1 - incomplete downloads
        }

        public SaveLocationChange onCompletion(Download d, boolean for_move, boolean on_event) {
            DownloadManager dm = ((DownloadImpl) d).getDownload();
            MovementInformation mi = getTagMovementInformation(dm, COMPLETION_DETAILS);
            return determinePaths(dm, mi, for_move, false);
        }

        public SaveLocationChange testOnCompletion(Download d, boolean for_move, boolean on_event) {
            DownloadManager dm = ((DownloadImpl) d).getDownload();
            MovementInformation mi = getTagMovementInformation(dm, COMPLETION_DETAILS);
            return determinePaths(dm, mi, for_move, true);
        }

        public SaveLocationChange onRemoval(Download d, boolean for_move, boolean on_event) {
            DownloadManager dm = ((DownloadImpl) d).getDownload();
            return determinePaths(dm, REMOVAL_DETAILS, for_move, false);
        }

        public boolean isInDefaultSaveDir(Download d) {
            DownloadManager dm = ((DownloadImpl) d).getDownload();
            return DownloadManagerDefaultPaths.isInDefaultDownloadDir(dm);
        }
    };

    private final static MovementInformation COMPLETION_DETAILS;
    private final static MovementInformation REMOVAL_DETAILS;
    private final static MovementInformation[] UPDATE_FOR_MOVE_DETAILS;
    private final static TargetSpecification[] DEFAULT_DIRS;

    private final static String SUBDIR_PARAM = "File.move.subdir_is_default";

    static {
        SourceSpecification source;
        TargetSpecification dest;
        TransferSpecification trans;
        MovementInformation mi_1, mi_2;

        /**
         * There are three sets of directories that we consider a "default" directory (perhaps it should just be two):
         * 
         * - default save dir - completed save dir - removed save dir
         */
        DEFAULT_DIRS = new TargetSpecification[3];
        dest = new TargetSpecification();
        dest.setBoolean("enabled", true);
        dest.setString("target", "Default save path");
        dest.setContext("default save dir");
        DEFAULT_DIRS[0] = dest;

        // First - download completion details.
        source = new SourceSpecification();
        source.setBoolean("default dir", "Move Only When In Default Save Dir");
        source.setBoolean("default subdir", SUBDIR_PARAM);
        source.setBoolean("incomplete dl", false);

        dest = new TargetSpecification();
        dest.setBoolean("enabled", "Move Completed When Done");
        dest.setString("target", "Completed Files Directory");
        dest.setContext("completed files dir");

        trans = new TransferSpecification();
        trans.setBoolean("torrent", "Move Torrent When Done");

        mi_1 = new MovementInformation(source, dest, trans, "Move on completion");
        COMPLETION_DETAILS = mi_1;
        DEFAULT_DIRS[1] = dest;

        // Next - download removal details.
        source = new SourceSpecification();
        source.setBoolean("default dir", "File.move.download.removed.only_in_default");
        source.setBoolean("default subdir", SUBDIR_PARAM);
        source.setBoolean("incomplete dl", false);

        dest = new TargetSpecification();
        dest.setBoolean("enabled", "File.move.download.removed.enabled");
        dest.setString("target", "File.move.download.removed.path");
        dest.setContext("removed files dir");

        trans = new TransferSpecification();
        trans.setBoolean("torrent", "File.move.download.removed.move_torrent");

        mi_1 = new MovementInformation(source, dest, trans, "Move on removal");
        REMOVAL_DETAILS = mi_1;
        DEFAULT_DIRS[2] = dest;

        /**
         * Next - updating the current path (complete dl's first)
         * 
         * We instantiate the "update incomplete download" source first, and then we instantiate the "update complete download", but when we process,
         * we will do the complete download bit first.
         * 
         * We do this, because in the "update incomplete download" section, completed downloads are enabled for it. And the reason it is, is because
         * this will allow the code to behave properly if move on completion is not enabled.
         * 
         * Complete downloads apply to this bit, just in case the "move on completion" section isn't active.
         */
        source = new SourceSpecification();
        source.updateSettings(COMPLETION_DETAILS.source.getSettings());
        source.setBoolean("default dir", true);

        mi_1 = new MovementInformation(source, COMPLETION_DETAILS.target, COMPLETION_DETAILS.transfer, "Update completed download");

        // Now incomplete downloads. We have to define completely new settings for
        // it, since we've never defined it before.
        source = new SourceSpecification();
        source.setBoolean("default dir", true); // Must be in default directory to update.
        source.setBoolean("default subdir", SUBDIR_PARAM);
        source.setBoolean("incomplete dl", true);

        dest = new TargetSpecification();
        dest.setBoolean("enabled", true);
        dest.setString("target", "Default save path");

        trans = new TransferSpecification();
        trans.setBoolean("torrent", false);

        // Rest of the settings are the same.
        mi_2 = new MovementInformation(source, dest, trans, "Update incomplete download");
        UPDATE_FOR_MOVE_DETAILS = new MovementInformation[] { mi_1, mi_2 };

    }

    private static MovementInformation getTagMovementInformation(DownloadManager dm, MovementInformation def_mi) {
        List<Tag> dm_tags = TagManagerFactory.getTagManager().getTagsForTaggable(dm);

        if (dm_tags == null || dm_tags.size() == 0) {

            return (def_mi);
        }

        List<Tag> applicable_tags = new ArrayList<Tag>();

        for (Tag tag : dm_tags) {

            if (tag.getTagType().hasTagTypeFeature(TagFeature.TF_FILE_LOCATION)) {

                TagFeatureFileLocation fl = (TagFeatureFileLocation) tag;

                if (fl.supportsTagMoveOnComplete()) {

                    File move_to = fl.getTagMoveOnCompleteFolder();

                    if (move_to != null) {

                        if (!move_to.exists()) {

                            move_to.mkdirs();
                        }

                        if (move_to.isDirectory() && move_to.canWrite()) {

                            applicable_tags.add(tag);

                        } else {

                            logInfo("Ignoring invalid tag move-to location: " + move_to, dm);
                        }
                    }
                }
            }
        }

        if (applicable_tags.size() == 0) {

            return (def_mi);

        } else if (applicable_tags.size() > 1) {

            Collections.sort(applicable_tags, new Comparator<Tag>() {
                public int compare(Tag o1, Tag o2) {
                    return (o1.getTagID() - o2.getTagID());
                }
            });

            String str = "";

            for (Tag tag : applicable_tags) {

                str += (str.length() == 0 ? "" : ", ") + tag.getTagName(true);
            }

            logInfo("Multiple applicable tags found: " + str + " - selecting first", dm);
        }

        Tag tag_target = applicable_tags.get(0);

        TagFeatureFileLocation fl = (TagFeatureFileLocation) tag_target;

        File move_to = fl.getTagMoveOnCompleteFolder();

        if (move_to != null) {

            SourceSpecification source = new SourceSpecification();
            source.setBoolean("default dir", "Move Only When In Default Save Dir");
            source.setBoolean("default subdir", SUBDIR_PARAM);
            source.setBoolean("incomplete dl", false);

            TargetSpecification dest = new TargetSpecification();
            dest.setBoolean("enabled", true);
            dest.setString("target_raw", move_to.getAbsolutePath());
            dest.setContext("Tag '" + tag_target.getTagName(true) + "' move-on-complete directory");

            TransferSpecification trans = new TransferSpecification();
            trans.setBoolean("torrent", "Move Torrent When Done");

            MovementInformation tag_mi = new MovementInformation(source, dest, trans, "Tag Move on Completion");

            return (tag_mi);
        }

        return (def_mi);
    }

    private static interface ContextDescriptor {
        public String getContext();
    }

    private static String normaliseRelativePathPart(String name) {
        name = name.trim();
        if (name.length() == 0) {
            return "";
        }
        if (name.equals(".") || name.equals("..")) {
            return null;
        }
        return FileUtil.convertOSSpecificChars(name, false).trim();
    }

    public static File normaliseRelativePath(File path) {
        if (path.isAbsolute()) {
            return null;
        }

        File parent = path.getParentFile();
        String child_name = normaliseRelativePathPart(path.getName());
        if (child_name == null) {
            return null;
        }

        // Simple one-level path.
        if (parent == null) {
            return new File(child_name);
        }

        ArrayList parts = new ArrayList();
        parts.add(child_name);

        String filepart = null;
        while (parent != null) {
            filepart = normaliseRelativePathPart(parent.getName());
            if (filepart == null) {
                return null;
            } else if (filepart.length() == 0) {/* continue */
            } else {
                parts.add(0, filepart);
            }
            parent = parent.getParentFile();
        }

        StringBuffer sb = new StringBuffer((String) parts.get(0));
        for (int i = 1; i < parts.size(); i++) {
            sb.append(File.separatorChar);
            sb.append(parts.get(i));
        }

        return new File(sb.toString());
    }

    private static File[] getDefaultDirs() {
        List results = new ArrayList();
        File location = null;
        TargetSpecification ts = null;
        for (int i = 0; i < DEFAULT_DIRS.length; i++) {
            ts = DEFAULT_DIRS[i];
            location = ts.getTarget(null, ts);
            if (location != null) {
                results.add(location);
            }
        }
        return (File[]) results.toArray(new File[results.size()]);
    }

    /**
     * This does the guts of determining appropriate file paths.
     */
    private static SaveLocationChange determinePaths(DownloadManager dm, MovementInformation mi, boolean check_source, boolean is_test) {
        boolean proceed = !check_source || mi.source.matchesDownload(dm, mi, is_test);
        if (!proceed) {
            logInfo("Cannot consider " + describe(dm, mi) + " - does not match source criteria.", dm);
            return null;
        }

        File target_path = mi.target.getTarget(dm, mi);
        if (target_path == null) {
            logInfo("Unable to determine an appropriate target for " + describe(dm, mi) + ".", dm);
            return null;
        }

        logInfo("Determined path for " + describe(dm, mi) + ".", dm);
        return mi.transfer.getTransferDetails(dm, mi, target_path);
    }

    static boolean isInDefaultDownloadDir(DownloadManager dm) {
        // We don't create this object properly, but just enough to get it
        // to be usable.
        SourceSpecification source = new SourceSpecification();
        source.setBoolean("default subdir", SUBDIR_PARAM);
        return source.checkDefaultDir(dm.getSaveLocation().getParentFile(), getDefaultDirs());
    }

    private static class MovementInformation implements ContextDescriptor {
        final SourceSpecification source;
        final TargetSpecification target;
        final TransferSpecification transfer;
        final String title;

        MovementInformation(SourceSpecification source, TargetSpecification target, TransferSpecification transfer, String title) {
            this.source = source;
            this.target = target;
            this.transfer = transfer;
            this.title = title;
        }

        public String getContext() {
            return title;
        }
    }

    private abstract static class ParameterHelper implements ContextDescriptor {
        private Map settings = new HashMap();
        private String context = null;

        protected boolean getBoolean(String key) {
            Object result = this.settings.get(key);
            if (result == null) {
                throw new RuntimeException("bad key: " + key);
            }
            if (result instanceof Boolean) {
                return ((Boolean) result).booleanValue();
            }
            return COConfigurationManager.getBooleanParameter((String) result);
        }

        protected void setBoolean(String key, boolean value) {
            settings.put(key, Boolean.valueOf(value));
        }

        protected void setBoolean(String key, String param) {
            settings.put(key, param);
        }

        protected void setString(String key, String param) {
            settings.put(key, param);
        }

        protected String getStringRaw(String key) {
            return ((String) this.settings.get(key));
        }

        protected String getString(String key) {
            String result = (String) this.settings.get(key);
            if (result == null) {
                throw new RuntimeException("bad key: " + key);
            }

            // This try-catch should be removed, it's only here for debugging purposes.
            return COConfigurationManager.getStringParameter(result);
        }

        public Map getSettings() {
            return this.settings;
        }

        public void updateSettings(Map settings) {
            this.settings.putAll(settings);
        }

        public String getContext() {
            return this.context;
        }

        public void setContext(String context) {
            this.context = context;
        }
    }

    private static class SourceSpecification extends ParameterHelper {

        public boolean matchesDownload(DownloadManager dm, ContextDescriptor context, boolean ignore_completeness) {
            if (this.getBoolean("default dir")) {
                logInfo("Checking if " + describe(dm, context) + " is inside default dirs.", dm);
                File[] default_dirs = getDefaultDirs();
                File current_location = dm.getSaveLocation().getParentFile();

                /**
                 * Very very rare, but I have seen this on fscked up downloads which don't appear to have a blank / malformed download path.
                 */
                if (current_location == null) {
                    logWarn(describe(dm, context) + " appears to have a malformed save directory, skipping.", dm);
                    return false;
                }

                if (!this.checkDefaultDir(current_location, default_dirs)) {
                    logWarn(describe(dm, context) + " doesn't exist in any of the following default directories" + " (current dir: "
                            + current_location + ", subdirectories checked: " + this.getBoolean("default subdir") + ") - "
                            + Arrays.asList(default_dirs), dm);
                    return false;
                }
                logInfo(describe(dm, context) + " does exist inside default dirs.", dm);
            }

            // Does it work for incomplete downloads?
            if (!dm.isDownloadComplete(false)) {
                boolean can_move = ignore_completeness || this.getBoolean("incomplete dl");
                String log_message = describe(dm, context) + " is incomplete which is " + ((can_move) ? "" : "not ") + "an appropriate state.";
                if (!can_move) {
                    logInfo(log_message, dm);
                    return false;
                }
            }

            return true;
        }

        public boolean checkDefaultDir(File location, File[] default_dirs) {
            location = FileUtil.canonise(location);
            boolean subdir = this.getBoolean("default subdir");
            for (int i = 0; i < default_dirs.length; i++) {
                if (subdir) {
                    if (FileUtil.isAncestorOf(default_dirs[i], location)) {
                        return true;
                    }
                } else {
                    if (default_dirs[i].equals(location)) {
                        return true;
                    }
                }
            }
            return false;
        }

    }

    private static class TargetSpecification extends ParameterHelper {

        public File getTarget(DownloadManager dm, ContextDescriptor cd) {
            // logInfo("Calculating target location for " + describe(dm, cd), lr);
            if (!this.getBoolean("enabled")) {
                logInfo("Target for " + describe(dm, cd) + " is not enabled.", dm);
                return null;
            }
            String location = getStringRaw("target_raw");
            if (location == null) {
                location = this.getString("target").trim();
            }
            if (location.length() == 0) {
                logInfo("No explicit target for " + describe(dm, cd) + ".", dm);
                return null;
            }

            File target = new File(FileUtil.getCanonicalFileName(location));
            String relative_path = null;

            if (dm != null && dm.getDownloadState() != null) {
                relative_path = dm.getDownloadState().getRelativeSavePath();
            }

            if (relative_path != null && relative_path.length() > 0) {
                logInfo("Consider relative save path: " + relative_path, dm);

                // Doesn't matter if File.separator is required or not, it seems to
                // remove duplicate file separators.
                target = new File(target.getPath() + File.separator + relative_path);
            }
            return target;
        }

    }

    private static class TransferSpecification extends ParameterHelper {

        public SaveLocationChange getTransferDetails(DownloadManager dm, ContextDescriptor cd, File target_path) {

            if (target_path == null) {
                throw new NullPointerException();
            }

            SaveLocationChange result = new SaveLocationChange();
            result.download_location = target_path;
            if (this.getBoolean("torrent")) {
                result.torrent_location = target_path;
            }
            return result;
        }

    }

    public static File getCompletionDirectory(DownloadManager dm) {
        return COMPLETION_DETAILS.target.getTarget(dm, null);
    }

    static String describe(DownloadManager dm, ContextDescriptor cs) {
        if (cs == null) {
            return describe(dm);
        }
        if (dm == null) {
            return "\"" + cs.getContext() + "\"";
        }
        return "\"" + dm.getDisplayName() + "\" with regard to \"" + cs.getContext() + "\"";
    }

}